/*
 * This file is part of aCSV.
 *
 * aCSV is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * aCSV is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with aCSV.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.apelle.acsv;

import com.apelle.acsv.annotations.CSV;
import com.apelle.acsv.annotations.CSVBegin;
import com.apelle.acsv.annotations.CSVCase;
import com.apelle.acsv.annotations.CSVEnd;
import org.apache.log4j.Logger;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author FreeMan
 * @version $Id$
 */
public class CSVProcessor {
    private static final Logger LOGGER;

    static {
        LOGGER = Logger.getLogger(CSVProcessor.class.getName());
    }

    private CSV csvInformation;
    private Map<Integer, CaseHolder> cases;
    private Map<Integer, OutputStream> streams;
    private CaseHolder defaultMethod;
    private Method beginMethod;
    private Method endMethod;

    private Object processor;

    public CSVProcessor(String className) throws ClassNotFoundException, FileNotFoundException, IllegalAccessException, InstantiationException {
        int defaultCounter = 0;
        int beginCounter = 0;
        int endCounter = 0;
        beginMethod = null;
        endMethod = null;
        Class<?> clazz = Class.forName(className);

        processor = clazz.newInstance();


        csvInformation = clazz.getAnnotation(CSV.class);

        Method[] methods = clazz.getMethods();
        cases = new TreeMap<Integer, CaseHolder>();
        streams = new HashMap<Integer, OutputStream>();


        for (Method m : methods) {
            CSVCase csvCase = m.getAnnotation(CSVCase.class);

            if (csvCase != null) {
                if (csvCase.defaultCase()) {
                    defaultMethod = new CaseHolder(csvCase, m);
                    defaultCounter++;
                } else {
                    cases.put(csvCase.caseNum(), new CaseHolder(csvCase, m));
                }

                if (defaultCounter > 1) {
                    LOGGER.fatal("Only one Method could be used as default case");
                    throw new RuntimeException("One one Method could be used as default case");
                }

                if (csvCase.type().equals(CSVCase.OutputType.FILE)) {
                    streams.put(csvCase.caseNum(), new FileOutputStream(new File(csvCase.file())));
                }
            }

            if (m.getAnnotation(CSVBegin.class) != null) {
                beginMethod = m;
                beginCounter++;
            }

            if (m.getAnnotation(CSVEnd.class) != null) {
                endMethod = m;
                endCounter++;
            }

            if (beginCounter > 1) {
                LOGGER.fatal("Only one Method could be used as BEGIN");
                throw new RuntimeException("Only one Method could be used as BEGIN");
            }
            if (endCounter > 1) {
                LOGGER.fatal("Only one Method could be used as END");
                throw new RuntimeException("Only one Method could be used as END");
            }
        }

    }

    public void process(String inputFile) throws IOException, InvocationTargetException, IllegalAccessException {
        long startTime = System.currentTimeMillis();
        LOGGER.info("Start Process");

        if (beginMethod != null) {
            LOGGER.info("Running begin");
            beginMethod.invoke(processor);
        }


        BufferedReader br = new BufferedReader(new FileReader(inputFile));

        String line;

        while ((line = br.readLine()) != null) {
            String[] records = unpackLine(line);

            boolean isMatch = false;
            for (CaseHolder caseHolder : cases.values()) {
                CSVCase csvCase = caseHolder.getCsvCase();
                isMatch = false;


                Pattern p = Pattern.compile(csvCase.regexp());

                Matcher match;

                switch (csvCase.matchType()) {
                    case LINE:
                        match = p.matcher(line);
                        break;
                    case FIELD:
                        match = p.matcher(records[csvCase.fieldToMatch()]);
                        break;
                    default:
                        LOGGER.fatal("Match type not found: " + csvCase.matchType());
                        throw new RuntimeException("Match time not found: " + csvCase.matchType());
                }

                Method method = caseHolder.getMethod();
                LOGGER.info(method.getName() + ": " + csvCase.description());


                if (match.matches()) {
                    isMatch = true;
                    LOGGER.info(" -> match!");
                    LOGGER.info(" -> LINE: " + line);
                    LOGGER.trace(" -> IN: " + Arrays.toString(records));
                    LineHolder lh = new LineHolder(line, records);
                    String[] outFields = (String[]) method.invoke(processor, lh);
                    LOGGER.trace(" -> OUT: " + Arrays.toString(records));
                    String outLine = packLine(outFields) + "\n";

                    switch (csvCase.type()) {
                        case FILE:
                            streams.get(csvCase.caseNum()).write(outLine.getBytes());
                            break;
                        case SYSTEM_ERR:
                            System.err.println(outLine);
                            break;

                        case SYSTEM_OUT:
                            System.out.println(outLine);
                            break;
                        default:
                            LOGGER.fatal("File output not valid: " + csvCase.type());
                    }
                } else {
                    LOGGER.info(" -> NOT match!");
                }
            }

            if (!isMatch) {
                Method method = defaultMethod.getMethod();
                CSVCase csvCase = defaultMethod.getCsvCase();
                LOGGER.info(" -> default match!");
                LOGGER.info(" -> LINE: " + line);
                LOGGER.trace(" -> IN: " + Arrays.toString(records));
                LineHolder lh = new LineHolder(line, records);
                String[] outFields = (String[]) method.invoke(processor, lh);
                LOGGER.trace(" -> OUT: " + Arrays.toString(records));
                String outLine = packLine(outFields) + "\n";

                switch (csvCase.type()) {
                    case FILE:
                        streams.get(csvCase.caseNum()).write(outLine.getBytes());
                        break;
                    case SYSTEM_ERR:
                        System.err.println(outLine);
                        break;

                    case SYSTEM_OUT:
                        System.out.println(outLine);
                        break;
                    default:
                        LOGGER.fatal("File output not valid: " + csvCase.type());
                }
            }

        }
        br.close();
        for (OutputStream stream : streams.values()) {
            stream.close();
            LOGGER.trace("close stream");
        }

        if (endMethod != null) {
            LOGGER.info("Running end");
            endMethod.invoke(processor);
        }


        long time = System.currentTimeMillis() - startTime;

        LOGGER.info("Process completed [" + time + " ms]");
    }


    private String[] unpackLine(String line) {
        if (line == null) {
            throw new IllegalArgumentException("Line could not be null");
        }

        String[] split = line.split(csvInformation.inputFieldSep());
        if (split == null)
            return new String[]{};
        else
            return split;
    }

    private String packLine(String[] records) {
        if (records == null) {
            throw new IllegalArgumentException("records could not be null");
        }

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < records.length; i++) {
            sb.append(records[i]);

            if (i != records.length - 1) {
                sb.append(csvInformation.outputFieldSep());
            }
        }
        return sb.toString();
    }

}
